// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <X11/extensions/Xrandr.h>
#include <stdint.h>

#undef Bool
#undef None

#include "base/macros.h"
#include "base/test/simple_test_tick_clock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/chromeos/x11/display_mode_x11.h"
#include "ui/display/chromeos/x11/display_snapshot_x11.h"
#include "ui/display/chromeos/x11/native_display_delegate_x11.h"
#include "ui/display/chromeos/x11/native_display_event_dispatcher_x11.h"

namespace ui {

namespace {

    DisplaySnapshotX11* CreateOutput(int64_t id,
        DisplayConnectionType type,
        RROutput output,
        RRCrtc crtc)
    {
        static const DisplayModeX11* kDefaultDisplayMode = new DisplayModeX11(gfx::Size(1, 1), false, 60.0f, 20);

        DisplaySnapshotX11* snapshot = new DisplaySnapshotX11(
            id,
            gfx::Point(0, 0),
            gfx::Size(0, 0),
            type,
            false,
            false,
            std::string(),
            std::vector<const DisplayMode*>(1, kDefaultDisplayMode),
            std::vector<uint8_t>(),
            kDefaultDisplayMode,
            NULL,
            output,
            crtc,
            0);

        return snapshot;
    }

    DisplaySnapshotX11* CreateExternalOutput(RROutput output, RRCrtc crtc)
    {
        return CreateOutput(static_cast<int64_t>(output),
            DISPLAY_CONNECTION_TYPE_UNKNOWN,
            output,
            crtc);
    }

    DisplaySnapshotX11* CreateInternalOutput(RROutput output, RRCrtc crtc)
    {
        return CreateOutput(0,
            DISPLAY_CONNECTION_TYPE_INTERNAL,
            output,
            crtc);
    }

    class TestHelperDelegate : public NativeDisplayDelegateX11::HelperDelegate {
    public:
        TestHelperDelegate();
        ~TestHelperDelegate() override;

        int num_calls_update_xrandr_config() const
        {
            return num_calls_update_xrandr_config_;
        }

        int num_calls_notify_observers() const { return num_calls_notify_observers_; }

        void set_cached_outputs(const std::vector<DisplaySnapshot*>& outputs)
        {
            cached_outputs_ = outputs;
        }

        // NativeDisplayDelegateX11::HelperDelegate overrides:
        void UpdateXRandRConfiguration(const base::NativeEvent& event) override;
        const std::vector<DisplaySnapshot*>& GetCachedDisplays() const override;
        void NotifyDisplayObservers() override;

    private:
        int num_calls_update_xrandr_config_;
        int num_calls_notify_observers_;

        std::vector<DisplaySnapshot*> cached_outputs_;

        DISALLOW_COPY_AND_ASSIGN(TestHelperDelegate);
    };

    TestHelperDelegate::TestHelperDelegate()
        : num_calls_update_xrandr_config_(0)
        , num_calls_notify_observers_(0)
    {
    }

    TestHelperDelegate::~TestHelperDelegate() { }

    void TestHelperDelegate::UpdateXRandRConfiguration(
        const base::NativeEvent& event)
    {
        ++num_calls_update_xrandr_config_;
    }

    const std::vector<DisplaySnapshot*>& TestHelperDelegate::GetCachedDisplays()
        const
    {
        return cached_outputs_;
    }

    void TestHelperDelegate::NotifyDisplayObservers()
    {
        ++num_calls_notify_observers_;
    }

    ////////////////////////////////////////////////////////////////////////////////
    // NativeDisplayEventDispatcherX11Test

    class NativeDisplayEventDispatcherX11Test : public testing::Test {
    public:
        NativeDisplayEventDispatcherX11Test();
        ~NativeDisplayEventDispatcherX11Test() override;

    protected:
        void DispatchScreenChangeEvent();
        void DispatchOutputChangeEvent(RROutput output,
            RRCrtc crtc,
            RRMode mode,
            bool connected);

        int xrandr_event_base_;
        scoped_ptr<TestHelperDelegate> helper_delegate_;
        scoped_ptr<NativeDisplayEventDispatcherX11> dispatcher_;
        base::SimpleTestTickClock* test_tick_clock_; // Owned by |dispatcher_|.

    private:
        DISALLOW_COPY_AND_ASSIGN(NativeDisplayEventDispatcherX11Test);
    };

    NativeDisplayEventDispatcherX11Test::NativeDisplayEventDispatcherX11Test()
        : xrandr_event_base_(10)
        , helper_delegate_(new TestHelperDelegate())
        , dispatcher_(new NativeDisplayEventDispatcherX11(helper_delegate_.get(),
              xrandr_event_base_))
        , test_tick_clock_(new base::SimpleTestTickClock)
    {
        test_tick_clock_->Advance(base::TimeDelta::FromMilliseconds(1));
        dispatcher_->SetTickClockForTest(
            scoped_ptr<base::TickClock>(test_tick_clock_));
    }

    NativeDisplayEventDispatcherX11Test::~NativeDisplayEventDispatcherX11Test() { }

    void NativeDisplayEventDispatcherX11Test::DispatchScreenChangeEvent()
    {
        XRRScreenChangeNotifyEvent event = { 0 };
        event.type = xrandr_event_base_ + RRScreenChangeNotify;

        dispatcher_->DispatchEvent(reinterpret_cast<const PlatformEvent>(&event));
    }

    void NativeDisplayEventDispatcherX11Test::DispatchOutputChangeEvent(
        RROutput output,
        RRCrtc crtc,
        RRMode mode,
        bool connected)
    {
        XRROutputChangeNotifyEvent event = { 0 };
        event.type = xrandr_event_base_ + RRNotify;
        event.subtype = RRNotify_OutputChange;
        event.output = output;
        event.crtc = crtc;
        event.mode = mode;
        event.connection = connected ? RR_Connected : RR_Disconnected;

        dispatcher_->DispatchEvent(reinterpret_cast<const PlatformEvent>(&event));
    }

} // namespace

TEST_F(NativeDisplayEventDispatcherX11Test, OnScreenChangedEvent)
{
    DispatchScreenChangeEvent();
    EXPECT_EQ(1, helper_delegate_->num_calls_update_xrandr_config());
    EXPECT_EQ(0, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationOnFirstEvent)
{
    DispatchOutputChangeEvent(1, 10, 20, true);
    EXPECT_EQ(0, helper_delegate_->num_calls_update_xrandr_config());
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationAfterSecondEvent)
{
    DispatchOutputChangeEvent(1, 10, 20, true);

    // Simulate addition of the first output to the cached output list.
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(2, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationOnDisconnect)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(1, 10, 20, false);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationOnModeChange)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(1, 10, 21, true);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationOnSecondOutput)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test, CheckNotificationOnDifferentCrtc)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(1, 11, 20, true);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test,
    CheckNotificationOnSecondOutputDisconnect)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    outputs.push_back(CreateExternalOutput(2, 11));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(2, 11, 20, false);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test,
    AvoidDuplicateNotificationOnSecondOutputDisconnect)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    outputs.push_back(CreateExternalOutput(2, 11));
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(2, 11, 20, false);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());

    // Simulate removal of second output from cached output list.
    outputs.erase(outputs.begin() + 1);
    helper_delegate_->set_cached_outputs(outputs.get());

    DispatchOutputChangeEvent(2, 11, 20, false);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test,
    ForceUpdateAfterCacheExpiration)
{
    // +1 to compenstate a possible rounding error.
    const int kHalfOfExpirationMs = NativeDisplayEventDispatcherX11::kUseCacheAfterStartupMs / 2 + 1;

    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateExternalOutput(1, 10));
    outputs.push_back(CreateExternalOutput(2, 11));
    helper_delegate_->set_cached_outputs(outputs.get());

    EXPECT_EQ(0, helper_delegate_->num_calls_notify_observers());

    // Duplicated event will be ignored during the startup.
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(0, helper_delegate_->num_calls_notify_observers());

    test_tick_clock_->Advance(base::TimeDelta::FromMilliseconds(
        kHalfOfExpirationMs));

    // Duplicated event will still be ignored.
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(0, helper_delegate_->num_calls_notify_observers());

    // The startup timeout has been elapsed. Duplicated event
    // should not be ignored.
    test_tick_clock_->Advance(
        base::TimeDelta::FromMilliseconds(kHalfOfExpirationMs));
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());

    // Sending the same event immediately shoudldn't be ignored.
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(2, helper_delegate_->num_calls_notify_observers());

    // Advancing time further should not change the behavior.
    test_tick_clock_->Advance(base::TimeDelta::FromMilliseconds(
        kHalfOfExpirationMs));
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(3, helper_delegate_->num_calls_notify_observers());

    test_tick_clock_->Advance(
        base::TimeDelta::FromMilliseconds(kHalfOfExpirationMs));
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(4, helper_delegate_->num_calls_notify_observers());
}

TEST_F(NativeDisplayEventDispatcherX11Test,
    UpdateMissingExternalDisplayId)
{
    ScopedVector<DisplaySnapshot> outputs;
    outputs.push_back(CreateInternalOutput(1, 10));
    helper_delegate_->set_cached_outputs(outputs.get());

    ASSERT_EQ(0, helper_delegate_->num_calls_notify_observers());

    // Internal display's ID can be zero and not updated.
    DispatchOutputChangeEvent(1, 10, 20, true);
    EXPECT_EQ(0, helper_delegate_->num_calls_notify_observers());

    outputs.clear();
    outputs.push_back(CreateOutput(0, DISPLAY_CONNECTION_TYPE_UNKNOWN, 2, 11));
    helper_delegate_->set_cached_outputs(outputs.get());

    // External display should be updated if the id is zero.
    DispatchOutputChangeEvent(2, 11, 20, true);
    EXPECT_EQ(1, helper_delegate_->num_calls_notify_observers());
}

} // namespace ui
